/*

 Vinlic Frame 1.0.6
 Vinlic

*/

/*

 框架模块引入

 koa    koa2框架本体
 router    路由模块(特制)
 logger    控制台显示请求信息模块
 bodyParser    请求体接收转换模块
 xmlParser    XML数据接收转换模块
 cors    跨域请求模块
 multer    文件上传控制模块
 fs    文件操作模块
 argv    控制台参数接收模块
 Redis    Redis数据库操作封装模块
 MySQL    MySQL数据库操作封装模块
 common    框架工具封装类模块
 config    框架配置文件

*/

"use strict"
const koa = require('koa')
const router = require('koa-router')()
const logger = require('koa-logger')
const bodyParser  = require('koa-bodyparser')
const xmlParser = require('koa-xml-body')
//const cors = require('koa2-cors')
const multer = require('koa-multer')
const fs = require('fs-extra')
const argv = require('optimist').argv
const Redis = require('./libs/redis')
const MySQL = require('./libs/mysql')
const Amqp = require('./libs/amqp')
const Common = require('./libs/common')
const Auth = require('./libs/auth')
const Crypto = require('./libs/crypto')
const config = require(`./configs/${argv.dev ? 'development' : 'production'}`)

/*

 框架初始化

*/

//获得框架实例
const app = new koa()

//如果控制台传入了p参数则采用该参数为端口号否则使用默认端口
const serverPort = argv.p || config.server.port

//定时任务池
let cronPool = {}

//mysql连接池
let mysqlPool = {}

//amqp连接
let amqpPool = {}

//实例化工具类
const common = new Common({ config, fs, cronPool })

const auth = new Auth({ config })

const crypto = new Crypto({
	common,
	dataPrivateKey: fs.readFileSync('keys/cxxy_data_private.key', { encoding: 'utf-8' }),
	privateKey: fs.readFileSync('keys/cxxy_private.key', { encoding: 'utf-8' }),
	publicKey: fs.readFileSync('keys/cxxy_public.key', { encoding: 'utf-8' })
})

const dataKey = crypto.decryptDataKey(config.frame.dataKey)
const dataIv = crypto.decryptDataKey(config.frame.dataIv)

console.log('encrypt data key and iv:', dataKey, dataIv)

/*

 线程级信号监听处理
 
  exit 监听线程结束退出

*/

process.on('exit', (code) => {
	common.borderErr(`server is stopped(${code})`)
	//服务器异常退出写入log
	common.writeLog('error', `server was terminated(code:${code})`)
})

/*

 框架启动

*/

//写入服务器启动log
common.writeLog('info', 'server starting...')

//连接所有需要连接的MySQL数据库
config.mysql.dbs.forEach((dbName) => {
	let {dbPrefix, host, port, user, pwd, charset, encryptTableFields} = config.mysql 
	mysqlPool[(dbName.indexOf(dbPrefix) == -1 ? dbName : dbName.replace(`${dbPrefix}_`, ''))] = new MySQL(common, {host, port, user, pwd, name: dbName, charset, encryptTableFields}, crypto, dataKey, dataIv)
})

//连接所有需要连接的RabbitMQ服务器
for(let connName in config.amqp) {
	amqpPool[connName] = new Amqp(common, config.amqp[connName], crypto)
}

//初始化文件上传接收器
const uploader = multer({
	storage: multer.diskStorage({
		//文件临时保存路径  
		destination: (req, file, cb) => {
			cb(null, `${config.server.basePath}/${config.server.tmpPath}`)  
		},  
		//修改文件名称  
		filename: (req, file, cb) => {
			let tmpId = parseInt(Date.now())
			let fileFormat = (file.originalname).split(".") 
			cb(null, tmpId.toString())
		}
	}),
	fileFilter: (req, data, cb) => {
		req.upload = true
		let {names, mimes} = config.frame.fileFilter
		for(let f of names) {
			if(f == data.fieldname) {
				for(let m of mimes) {
					if(m == data.mimetype) {
						cb(null, true)
						return
					}
				}
			}
		}
		cb(null, false)
	},
	limits: {
		fileSize: config.frame.fileFilter.maxSize
	}
})

//注册中间件到框架
app.use(logger())
app.use(xmlParser({
	xmlOptions: {
		explicitArray: false
	},
	onerror: (err, ctx) => {
		console.error(err)
		common.writeLog('error', `parse xml data failed:${err.status} ${err.message}`)
		ctx.body = parseError('-2056', 'parse post xml data failed')
	}
})).use((ctx,next) => {
	console.log('request xml data:', ctx.request.body)
	ctx.xmlData = (ctx.request.body && ctx.request.body.xml ? ctx.request.body.xml : ctx.request.body)
	return next()
})
app.use(bodyParser())
// app.use(cors())

//输出框架显示
common.borderMsg(`RHRM Frame ${config.frame.version}`, 'Vinlic')

const parseData = (data = {}) => {
	return JSON.stringify({
		status: 'success',
		code: '0',
		data,
	})
}

const parseError = (code, msg, notPrint = false) => {
	if(!notPrint)
		console.error('interface return error:', code, msg)
	if(Object.prototype.toString.call(msg) === '[object String]') {
		return `{"status":"failed","code":"${code}","msg":"${msg}"}`
	}
	return JSON.stringify({
		status: 'failed',
		code,
		msg
	})
}

const resolve = (data = 0, noPromise = false) => {
	return noPromise ? data : Promise.resolve(data)
}

const reject =  (code, msg = 'handle failed', noPromise = false) => {
	return noPromise ? {code, msg} : Promise.reject({code, msg})
}

/**
 *
 * 注册处理件
 *
 * 用于供路由件调用的处理库
 *
 */

//处理件对象
let handles = {}
config.frame.handles.forEach((handleName) => {
	try {
		const Handle = require(`./handles/h_${handleName}`)
		handles[handleName] = new Handle()
		console.log(`handle ${handleName} loaded`)
	}
	catch(err) {
		console.error(err)
		console.error(`handle ${handleName} load failed`)
		//写入注册处理件失败log
		common.writeLog('error', `handle ${handleName} load failed`)
	}
})

const routerExt = {
	echo: (data) => {
		return Promise.resolve({echo:  true, data})
	},
	data: (data, notPrint = true) => {
		if(!notPrint)
			console.log(data)
		return Promise.resolve({code: '0', data})
	},
	error: (code, msg) => {
		console.error(code, msg)
		return Promise.reject({code, msg})
	}
}

/*

 注册路由件

 处理请求的路由件

*/

config.frame.routers.forEach((routerName) => {
	try {
		const _router = require(`./routers/r_${routerName}`)
		for(let method in _router) {
			if(['all', 'get', 'post', 'put', 'delete', 'upload'].indexOf(method) == -1) {
				console.error(`${method} is invalid method`)
				continue
			}
			const pathArr = Object.keys(_router[method])
			if(pathArr.length == 0)
				continue
			
			const core = async (ctx, next) => {
				
				let rdb = {}
				let redis = {}
				let mdb = {}
				let mysql = {}

				try {

					if(!ctx.index && ctx.index != 0)
						ctx.index = 0
					const path = ctx.matched[ctx.index].path
					const _method = ctx.request.method.toLowerCase()
					const uri = path.substring(config.frame.uriPrefix.length)
					const uploadData = ctx.req.file
					if(ctx.req.upload && !uploadData) {
						ctx.body = parseError('-1034', 'upload file type invalid')
						return
					}
					if(ctx.matched[ctx.index].methods.indexOf(_method.toUpperCase()) == -1 || ((!_router[_method] || !_router[_method][uri] || Object.prototype.toString.call(_router[_method][uri])!= '[object AsyncFunction]') && (!_router['all'] || !_router['all'][uri] || Object.prototype.toString.call(_router[_method][uri]) != '[object AsyncFunction]') && (uploadData && (!_router['upload'] || !_router['upload'][uploadData.fieldname] || !_router['upload'][uploadData.fieldname][uri] || Object.prototype.toString.call(_router['upload'][uploadData.fieldname][uri]) != '[object AsyncFunction]')))) {
						ctx.index++
						return await core(ctx, next)
					}
					ctx.originIP = ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] || (ctx.ip ? ctx.ip.substr(7, ctx.ip.length - 1) : '0.0.0.0')
					ctx.uri = uri
					ctx.request.method = _method
					ctx.queryData = ctx.query
					ctx.bodyData = Object.keys(ctx.request.body).length > 0 ? ctx.request.body : (ctx.req.body || {})
					ctx.file = uploadData
					ctx.crypto = crypto

					for(let name in config.redis) {
						redis[name] = {}
						for(let item of ['get', 'set', 'delete', 'expire', 'find', 'trans', 'commit']) {
							redis[name][item] = async (...args) => {
								if(!rdb[name]) {
									rdb[name] = new Redis(common, name, config.redis[name], crypto, dataKey, dataIv)
								}
								console.log(name, item, ...args)
								return await rdb[name][item](...args)
							}
						}
					}
					common.redis = redis
					Object.assign(auth, {config, common, uri: ctx.uri, method: ctx.request.method, queryData: ctx.queryData, bodyData: ctx.bodyData, crypto, redis, resolve, reject, ctx})
					
					for(let name in mysqlPool) {
						mysql[name] = {}
						for(let item of ['insert', 'delete', 'select', 'update', 'execute', 'createTable', 'tableExists', 'trans', 'cmt', 'encryptData', 'decryptData']) {
							mysql[name][item] = async (...args) => {
								if(!mdb[name]) {
									mdb[name] = await mysqlPool[name].getConn()
									return new Promise((resolve, reject) => {
										mdb[name].ping((err) => {
											if(err) {
												console.error(`mysql ${name} state is closed, now reconnect`, err)
												mysqlPool[name].close().then(() => {
													config.mysql.dbs.forEach((dbName) => {
														let {dbPrefix, host, port, user, pwd, charset, encryptTableFields} = config.mysql 
														mysqlPool[(dbName.indexOf(dbPrefix) == -1 ? dbName : dbName.replace(`${dbPrefix}_`, ''))] = new MySQL(common, {host, port, user, pwd, name: dbName, charset, encryptTableFields}, crypto, dataKey, dataIv)
													})
													mysqlPool[name].getConn().then((conn) => {
														mdb[name] = conn
														if(Object.prototype.toString.call(mdb[name][item]) == '[object AsyncFunction]')
															mdb[name][item](...args).then(resolve).catch(reject)
														else {
															try {
																resolve(mdb[name][item](...args))
															}
															catch(err) {
																reject(err)
															}
														}
													})
												}).catch((err) => {
													console.error(err)
													reject(err)
												})
											}
											else {
												if(Object.prototype.toString.call(mdb[name][item]) == '[object AsyncFunction]')
													mdb[name][item](...args).then(resolve).catch(reject)
												else {
													try {
														resolve(mdb[name][item](...args))
													}
													catch(err) {
														reject(err)
													}
												}
											}
										})
									})
								}
								if(Object.prototype.toString.call(mdb[name][item]) == '[object AsyncFunction]')
									return await mdb[name][item](...args)
								else
									return mdb[name][item](...args)
							}
						}
					}
					//
					for(let name in handles) {
						Object.assign(handles[name], {common, crypto, config, fs, mysql, redis, amqp: amqpPool, cron: cronPool, resolve, reject, hd: handles, dataKey})
					}
					Object.assign(ctx, {hd: handles, common, config, auth}, routerExt)
					//
					let target
					console.log(`${ctx.request.host} request ${ctx.request.url} interface`)
					if(ctx.req.upload)
						target = _router['upload'][uploadData.fieldname][uri]
					else if(_router['all'] && _router['all'][uri])
						target = _router['all'][uri]
					else 
						target = _router[_method][uri]
					console.log(target, uploadData, _method, uri)
					if(!target) {
						for(let name in rdb) {
							rdb[name].quit()
							delete rdb[name]
						}
						rdb = {}
						ctx.body = parseError('-1029', 'router is undefined')
						return
					}
					const result = await target(ctx, '&next')
					if(result == '&next') {
						return await core(ctx, next)
					}
					else {
						for(let name in rdb) {
							rdb[name].quit()
							delete rdb[name]
						}
						rdb = {}
						console.log('interface return data:', result)
						if(!result && result !== 0 && result !== false) {
							console.error(`${uri} interface is not return data`)
							ctx.status = 500
							ctx.body = parseError('-1003', `${uri} interface is not return data`)
							return
						}
						//如果要求原样输出则按原样输入
						if(ctx.anyOut) {
							ctx.body = result
							return
						}
						let {code, msg, data, echo} = result
						if(echo) {
							ctx.body = data
							return
						}
						let responseData = !code && code !== '0' ? parseData(result) : parseData(data)
						//如果数据采用加密则返回数据也加密
						if(ctx.resEncryptData) {
							const {key, iv} = ctx.resEncryptData
							responseData = {
								status: 'success',
								code: '0',
								encryptData: crypto.encryptAES(responseData, key, iv),
								iv
							}
						}
						ctx.body = responseData
					}
				}
				catch(err) {
					if(!err || !err.code || !err.msg) {
						console.error('server error:', err)
						err = {
							code: '-1001',
							msg: `server error`
						}
					}
					const {code, msg} = err
					console.error(`${ctx.request.method} ${ctx.request.url}  request failed:`, `[${code}]${msg}`)
					for(let name in rdb) {
						rdb[name].quit()
						delete rdb[name]
					}
					rdb = {}
					ctx.status = 500
					ctx.body = parseError(code, msg)
					common.writeLog('error', 'router handle data failed', err)
				}
			}
			if(method == 'upload') {
				for(let name in _router[method])
					router.post(Object.keys(_router[method][name]), uploader.single(name), core)
				continue
			}
			router[method](pathArr, core)
		}
		console.log(`router ${routerName} loaded`)
	}
	catch(err) {
		console.error(err)
		console.error(`router ${routerName} load failed`)
		//写入注册路由件失败log
		common.writeLog('error', `router ${handleName} load failed`)
	}
})
//注册路径前缀
router.prefix(config.frame.uriPrefix)
app.use(router.routes()).use(router.allowedMethods())

/*

 路由未命中处理
 
 [return] -1000 不支持的请求信息

 */

app.use((ctx, next) => {
	//过滤谷歌浏览器的烦人图标请求
	if(ctx.request.url == '/favicon.ico') {
		ctx.status = 404
		ctx.body = parseError('-1002', 'favicon is not support', true)
		return
	}
	console.error(`${ctx.request.method} ${ctx.request.url} is not support request`)
	const ip =  ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] || (ctx.ip ? ctx.ip.substr(7, ctx.ip.length - 1) : '0.0.0.0')
	console.log('request IP: %s', ip, ctx.headers)
	ctx.status = 405
	ctx.body = parseError('-1000', `${ctx.request.method} ${ctx.request.url} is not support request`)
})

/*

 框架错误处理

 发生意外错误时输出错误

*/

app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
	//写入服务器错误log
	common.writeLog('error', `server error ${err}`)
});

/*

 服务器端口监听处理
 
 正式启动框架并显示

*/

app.listen(serverPort, async () => {
	common.borderMsg('Vinlic Frame is running ^-^', `Port: ${serverPort}`)
	//写入服务器启动完毕log
	common.writeLog('info', `server started, port is ${serverPort}`)
})